package com.browseengine.bobo.facets;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import com.browseengine.bobo.api.BoboSegmentReader;
import com.browseengine.bobo.api.BrowseSelection;
import com.browseengine.bobo.api.BrowseSelection.ValueOperation;
import com.browseengine.bobo.api.FacetAccessible;
import com.browseengine.bobo.api.FacetSpec;
import com.browseengine.bobo.facets.filter.EmptyFilter;
import com.browseengine.bobo.facets.filter.RandomAccessAndFilter;
import com.browseengine.bobo.facets.filter.RandomAccessFilter;
import com.browseengine.bobo.facets.filter.RandomAccessNotFilter;
import com.browseengine.bobo.facets.filter.RandomAccessOrFilter;
import com.browseengine.bobo.sort.DocComparatorSource;

/**
 * FacetHandler definition
 *
 */
public abstract class FacetHandler<D> {
  public static class FacetDataNone implements Serializable {
    private static final long serialVersionUID = 1L;
    public static FacetDataNone instance = new FacetDataNone();

    private FacetDataNone() {
    }
  }

  protected final String _name;
  private final Set<String> _dependsOn;
  private final Map<String, FacetHandler<?>> _dependedFacetHandlers;
  private TermCountSize _termCountSize;

  public static enum TermCountSize {
    small, medium, large
  }

  /**
   * Constructor
   * @param name name
   * @param dependsOn Set of names of facet handlers this facet handler depend on for loading
   */
  public FacetHandler(String name, Set<String> dependsOn) {
    _name = name;
    _dependsOn = new HashSet<String>();
    if (dependsOn != null) {
      _dependsOn.addAll(dependsOn);
    }
    _dependedFacetHandlers = new HashMap<String, FacetHandler<?>>();
    _termCountSize = TermCountSize.large;
  }

  public FacetHandler<D> setTermCountSize(String termCountSize) {
    setTermCountSize(TermCountSize.valueOf(termCountSize.toLowerCase()));
    return this;
  }

  public FacetHandler<D> setTermCountSize(TermCountSize termCountSize) {
    _termCountSize = termCountSize;
    return this;
  }

  public TermCountSize getTermCountSize() {
    return _termCountSize;
  }

  /**
   * Constructor
   * @param name name
   */
  public FacetHandler(String name) {
    this(name, null);
  }

  /**
   * Gets the name
   * @return name
   */
  public final String getName() {
    return _name;
  }

  /**
   * Gets names of the facet handler this depends on
   * @return set of facet handler names
   */
  public final Set<String> getDependsOn() {
    return _dependsOn;
  }

  /**
   * Adds a list of depended facet handlers
   * @param facetHandler depended facet handler
   */
  public final void putDependedFacetHandler(FacetHandler<?> facetHandler) {
    _dependedFacetHandlers.put(facetHandler._name, facetHandler);
  }

  /**
   * Gets a depended facet handler
   * @param name facet handler name
   * @return facet handler instance
   */
  public final FacetHandler<?> getDependedFacetHandler(String name) {
    return _dependedFacetHandlers.get(name);
  }

  /**
   * Load information from an index reader, initialized by {@link BoboSegmentReader}
   * @param reader reader
   * @throws IOException
   */
  abstract public D load(BoboSegmentReader reader) throws IOException;

  public FacetAccessible merge(FacetSpec fspec, List<FacetAccessible> facetList) {
    return new CombinedFacetAccessible(fspec, facetList);
  }

  @SuppressWarnings("unchecked")
  public D getFacetData(BoboSegmentReader reader) {
    return (D) reader.getFacetData(_name);
  }

  public D load(BoboSegmentReader reader, BoboSegmentReader.WorkArea workArea) throws IOException {
    return load(reader);
  }

  public void loadFacetData(BoboSegmentReader reader, BoboSegmentReader.WorkArea workArea)
      throws IOException {
    reader.putFacetData(_name, load(reader, workArea));
  }

  public void loadFacetData(BoboSegmentReader reader) throws IOException {
    reader.putFacetData(_name, load(reader));
  }

  /**
   * Gets a filter from a given selection
   * @param sel selection
   * @return a filter
   * @throws IOException
   * @throws IOException
   */
  public RandomAccessFilter buildFilter(BrowseSelection sel) throws IOException {
    String[] selections = sel.getValues();
    String[] notSelections = sel.getNotValues();
    Properties prop = sel.getSelectionProperties();

    RandomAccessFilter filter = null;
    if (selections != null && selections.length > 0) {
      if (sel.getSelectionOperation() == ValueOperation.ValueOperationAnd) {
        filter = buildRandomAccessAndFilter(selections, prop);
        if (filter == null) {
          filter = EmptyFilter.getInstance();
        }
      } else {
        filter = buildRandomAccessOrFilter(selections, prop, false);
        if (filter == null) {
          return EmptyFilter.getInstance();
        }
      }
    }

    if (notSelections != null && notSelections.length > 0) {
      RandomAccessFilter notFilter = buildRandomAccessOrFilter(notSelections, prop, true);
      if (filter == null) {
        filter = notFilter;
      } else {
        RandomAccessFilter andFilter = new RandomAccessAndFilter(
            Arrays.asList(new RandomAccessFilter[] { filter, notFilter }));
        filter = andFilter;
      }
    }

    return filter;
  }

  abstract public RandomAccessFilter buildRandomAccessFilter(String value,
      Properties selectionProperty) throws IOException;

  public RandomAccessFilter buildRandomAccessAndFilter(String[] vals, Properties prop)
      throws IOException {
    ArrayList<RandomAccessFilter> filterList = new ArrayList<RandomAccessFilter>(vals.length);

    for (String val : vals) {
      RandomAccessFilter f = buildRandomAccessFilter(val, prop);
      if (f != null) {
        filterList.add(f);
      } else {
        return EmptyFilter.getInstance();
      }
    }

    if (filterList.size() == 1) return filterList.get(0);
    return new RandomAccessAndFilter(filterList);
  }

  public RandomAccessFilter buildRandomAccessOrFilter(String[] vals, Properties prop, boolean isNot)
      throws IOException {
    ArrayList<RandomAccessFilter> filterList = new ArrayList<RandomAccessFilter>(vals.length);

    for (String val : vals) {
      RandomAccessFilter f = buildRandomAccessFilter(val, prop);
      if (f != null && !(f instanceof EmptyFilter)) {
        filterList.add(f);
      }
    }

    RandomAccessFilter finalFilter;
    if (filterList.size() == 0) {
      finalFilter = EmptyFilter.getInstance();
    } else {
      finalFilter = new RandomAccessOrFilter(filterList);
    }

    if (isNot) {
      finalFilter = new RandomAccessNotFilter(finalFilter);
    }
    return finalFilter;
  }

  /**
   * Gets a FacetCountCollector
   * @param sel selection
   * @param fspec facetSpec
   * @return a FacetCountCollector
   */
  abstract public FacetCountCollectorSource getFacetCountCollectorSource(BrowseSelection sel,
      FacetSpec fspec);

  /**
   * Override this method if your facet handler have a better group mode like the SimpleFacetHandler.
   */
  public FacetCountCollectorSource getFacetCountCollectorSource(BrowseSelection sel,
      FacetSpec ospec, boolean groupMode) {
    return getFacetCountCollectorSource(sel, ospec);
  }

  /**
   * Gets the field value
   * @param id doc
   * @param reader index reader
   * @return array of field values
   * @see #getFieldValue(BoboSegmentReader,int)
   */
  abstract public String[] getFieldValues(BoboSegmentReader reader, int id);

  public int getNumItems(BoboSegmentReader reader, int id) {
    throw new UnsupportedOperationException("getNumItems is not supported for this facet handler: "
        + getClass().getName());
  }

  public Object[] getRawFieldValues(BoboSegmentReader reader, int id) {
    return getFieldValues(reader, id);
  }

  /**
   * Gets a single field value
   * @param id doc
   * @param reader index reader
   * @return first field value
   * @see #getFieldValues(BoboSegmentReader,int)
   */
  public String getFieldValue(BoboSegmentReader reader, int id) {
    return getFieldValues(reader, id)[0];
  }

  /**
   * builds a comparator to determine how sorting is done
   * @return a sort comparator
   */
  abstract public DocComparatorSource getDocComparatorSource();

  @Override
  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}
